/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs

This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.

You should have received a copy of the IBM Common Public
License along with this library
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;

#if XNA
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

// ************************************************************************* //
// Disable 'missing xml comment' warning for this file since it has been
// generated by .NET Reflector and I'm not going to comment this all
// just so I have to start from scratch again when XNA gets updated
#pragma warning disable 1591
// ************************************************************************* //

namespace Nuclex {

  public partial class GameControl : UserControl {

    // Events
    public event EventHandler Activated;

    public event EventHandler Deactivated;

    //public event EventHandler Disposed;

    public event EventHandler Exiting;

    // Methods
    public GameControl() {
      this.EnsureHost();
      this.gameComponents = new GameComponentCollection();
      this.gameComponents.ComponentAdded += new EventHandler<GameComponentCollectionEventArgs>(this.GameComponentAdded);
      this.gameComponents.ComponentRemoved += new EventHandler<GameComponentCollectionEventArgs>(this.GameComponentRemoved);
      this.Paint += new PaintEventHandler(DoPaint);
      this.clock = new GameClock();
      this.totalGameTime = TimeSpan.Zero;
      this.accumulatedElapsedGameTime = TimeSpan.Zero;
      this.lastFrameElapsedGameTime = TimeSpan.Zero;
      this.targetElapsedTime = TimeSpan.FromTicks((long)0x28b0a);
      this.inactiveSleepTime = TimeSpan.FromMilliseconds(20);

      this.graphicsDeviceManager = this.graphics;
      this.gameServices.AddService(
        typeof(IInputPublisherService), new ControlInputPublisher(this)
      );
    }

    #region Specialized UserControl Customizations
    protected override void OnLoad(EventArgs e) {
      // Upper part of Run() method
      base.OnLoad(e);

      this.graphicsDeviceManager = this.Services.GetService(typeof(IGraphicsDeviceManager)) as IGraphicsDeviceManager;
      if(this.graphicsDeviceManager != null) {
        this.graphicsDeviceManager.CreateDevice();
      }
      this.Initialize();
      this.inRun = true;
      try {
        this.BeginRun();
        this.gameTime = new GameTime(
          TimeSpan.Zero, TimeSpan.Zero,
          false
        );
        this.Update(this.gameTime);
        this.doneFirstUpdate = true;
        if(this.host != null) {
          this.host.PreRun();
        }
      }
      catch(Exception) {
        this.inRun = false;
        throw;
      }
    }
    protected override void WndProc(ref Message m) {
      //const int WM_CLOSE = 16;
      const int WM_DESTROY = 2;

      if(m.Msg == WM_DESTROY) {
        // Lower part of Run() method
        try {
          if(this.host != null) {
            this.host.PostRun();
          }
          this.EndRun();
        }
        finally {
          this.inRun = false;
        }
      }

      base.WndProc(ref m);
    }
    protected override void OnPaintBackground(PaintEventArgs e) {
      // We simply don't draw the background to prevent flickering
      //base.OnPaintBackground(e);
    }
    #endregion

    protected virtual bool BeginDraw() {
      if((this.graphicsDeviceManager != null) && !this.graphicsDeviceManager.BeginDraw()) {
        return false;
      }
      return true;
    }

    protected virtual void BeginRun() {
    }

    private void DeviceCreated(object sender, EventArgs e) {
      this.LoadGraphicsContent(true);
    }

    private void DeviceDisposing(object sender, EventArgs e) {
      this.UnloadGraphicsContent(true);
    }

    private void DeviceReset(object sender, EventArgs e) {
      this.LoadGraphicsContent(false);
    }

    private void DeviceResetting(object sender, EventArgs e) {
      this.UnloadGraphicsContent(false);
    }

    private void InternalDispose() {
      lock(this) {
        IGameComponent[] array = new IGameComponent[this.gameComponents.Count];
        this.gameComponents.CopyTo(array, 0);
        for(int i = 0; i < array.Length; i++) {
          IDisposable disposable = array[i] as IDisposable;
          if(disposable != null) {
            disposable.Dispose();
          }
        }
        IDisposable disposable2 = this.graphicsDeviceManager as IDisposable;
        if(disposable2 != null) {
          disposable2.Dispose();
        }
        this.UnhookDeviceEvents();

        // TODO: Check that base.Dispose actually fires the Disposed event!
        //if(this.Disposed != null) {
        //  this.Disposed(this, EventArgs.Empty);
        //}
      }
    }

    protected virtual void Draw(GameTime gameTime) {
      for(int i = 0; i < this.drawableComponents.Count; i++) {
        IDrawable drawable = this.drawableComponents[i];
        if(drawable.Visible) {
          drawable.Draw(gameTime);
        }
      }
    }

    private void DrawableDrawOrderChanged(object sender, EventArgs e) {
      IDrawable item = sender as IDrawable;
      this.drawableComponents.Remove(item);
      int num = this.drawableComponents.BinarySearch(item, DrawOrderComparer.Default);
      if(num < 0) {
        this.drawableComponents.Insert(~num, item);
      }
    }

    private void DrawFrame() {
      if((this.doneFirstUpdate /*&& !this.Window.IsMinimized*/) && this.BeginDraw()) {
        this.gameTime = new GameTime(
          this.clock.CurrentTime, this.elapsedRealTime,
          this.drawRunningSlowly
        );

        this.Draw(this.gameTime);
        this.EndDraw();
      }
    }

    protected virtual void EndDraw() {
      if(this.graphicsDeviceManager != null) {
        this.graphicsDeviceManager.EndDraw();
      }
    }

    protected virtual void EndRun() {
    }

    private void EnsureHost() {
      if(this.host == null) {
        this.host = new WindowsGameHost(this);
        this.host.Activated += new EventHandler(this.HostActivated);
        this.host.Deactivated += new EventHandler(this.HostDeactivated);
        this.host.Suspend += new EventHandler(this.HostSuspend);
        this.host.Resume += new EventHandler(this.HostResume);
        this.host.Idle += new EventHandler(this.HostIdle);
        this.host.Exiting += new EventHandler(this.HostExiting);
      }
    }

    public void Exit() {
      this.exitRequested = true;
      this.host.Exit();
    }

    ~GameControl() {
      this.Dispose(false);
    }

    private void GameComponentAdded(object sender, GameComponentCollectionEventArgs e) {
      if(this.inRun) {
        e.GameComponent.Initialize();
      }
      IUpdateable item = e.GameComponent as IUpdateable;
      if(item != null) {
        int num = this.updateableComponents.BinarySearch(item, UpdateOrderComparer.Default);
        if(num < 0) {
          this.updateableComponents.Insert(~num, item);
          //item.UpdateOrderChanged += new EventHandler();
        }
      }
      IDrawable drawable = e.GameComponent as IDrawable;
      if(drawable != null) {
        int num2 = this.drawableComponents.BinarySearch(drawable, DrawOrderComparer.Default);
        if(num2 < 0) {
          this.drawableComponents.Insert(~num2, drawable);
          //drawable.DrawOrderChanged += new EventHandler(this.DrawableDrawOrderChanged);
        }
      }
    }

    private void GameComponentRemoved(object sender, GameComponentCollectionEventArgs e) {
      IUpdateable item = e.GameComponent as IUpdateable;
      if(item != null) {
        this.updateableComponents.Remove(item);
        //item.UpdateOrderChanged -= new EventHandler(this.UpdateableUpdateOrderChanged);
      }
      IDrawable drawable = e.GameComponent as IDrawable;
      if(drawable != null) {
        this.drawableComponents.Remove(drawable);
        //drawable.DrawOrderChanged -= new EventHandler(this.DrawableDrawOrderChanged);
      }
    }

    private void HookDeviceEvents() {
      this.graphicsDeviceService = this.Services.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService;
      if(this.graphicsDeviceService != null) {
        //this.graphicsDeviceService.DeviceCreated += new EventHandler(this.DeviceCreated);
        //this.graphicsDeviceService.DeviceResetting += new EventHandler(this.DeviceResetting);
        //this.graphicsDeviceService.DeviceReset += new EventHandler(this.DeviceReset);
        //this.graphicsDeviceService.DeviceDisposing += new EventHandler(this.DeviceDisposing);
      }
    }

    private void HostActivated(object sender, EventArgs e) {
      if(!this.isActive) {
        this.isActive = true;
        this.OnActivated(this, EventArgs.Empty);
      }
    }

    private void HostDeactivated(object sender, EventArgs e) {
      if(this.isActive) {
        this.isActive = false;
        this.OnDeactivated(this, EventArgs.Empty);
      }
    }

    private void HostExiting(object sender, EventArgs e) {
      this.OnExiting(this, EventArgs.Empty);
    }

    private void HostIdle(object sender, EventArgs e) {
      this.Tick();
    }

    private void HostResume(object sender, EventArgs e) {
      this.clock.Resume();
    }

    private void HostSuspend(object sender, EventArgs e) {
      this.clock.Suspend();
    }

    protected virtual void Initialize() {
      for(int i = 0; i < this.gameComponents.Count; i++) {
        this.gameComponents[i].Initialize();
      }
      this.HookDeviceEvents();
      if((this.graphicsDeviceService != null) && (this.graphicsDeviceService.GraphicsDevice != null)) {
        this.LoadGraphicsContent(true);
      }
    }

    protected virtual void LoadGraphicsContent(bool loadAllContent) {
    }

    protected virtual void OnActivated(object sender, EventArgs args) {
      if(this.Activated != null) {
        this.Activated(this, args);
      }
    }

    protected virtual void OnDeactivated(object sender, EventArgs args) {
      if(this.Deactivated != null) {
        this.Deactivated(this, args);
      }
    }

    protected virtual void OnExiting(object sender, EventArgs args) {
      if(this.Exiting != null) {
        this.Exiting(null, args);
      }
    }

    private void DoPaint(object sender, EventArgs e) {
      this.DrawFrame();
    }

    public void Tick() {
      if(!this.exitRequested) {
        if(!this.isActive) {
          Thread.Sleep((int)this.inactiveSleepTime.TotalMilliseconds);
        } else 
        this.clock.Step();
        this.elapsedRealTime = this.clock.ElapsedTime;
        if(this.elapsedRealTime < TimeSpan.Zero) {
          this.elapsedRealTime = TimeSpan.Zero;
        }
        if(this.elapsedRealTime > this.maximumElapsedTime) {
          this.elapsedRealTime = this.maximumElapsedTime;
        }
        this.gameTime = new GameTime(
          this.clock.CurrentTime, this.elapsedRealTime,
          this.gameTime.IsRunningSlowly
        );
        this.drawRunningSlowly = false;
        if(this.isFixedTimeStep) {
          this.accumulatedElapsedGameTime += this.elapsedRealTime;
          long num = this.accumulatedElapsedGameTime.Ticks / this.targetElapsedTime.Ticks;
          this.accumulatedElapsedGameTime = TimeSpan.FromTicks(this.accumulatedElapsedGameTime.Ticks % this.targetElapsedTime.Ticks);
          this.lastFrameElapsedGameTime = TimeSpan.Zero;
          TimeSpan targetElapsedTime = this.targetElapsedTime;
          if(num > 0) {
            while(num > 1) {
              this.drawRunningSlowly = true;
              this.gameTime = new GameTime(
                this.gameTime.TotalGameTime, this.gameTime.ElapsedGameTime,
                true
              );
              num--;
              try {
                this.gameTime = new GameTime(
                  this.totalGameTime, targetElapsedTime,
                  this.gameTime.IsRunningSlowly
                );
                this.Update(this.gameTime);
                continue;
              }
              finally {
                this.lastFrameElapsedGameTime += targetElapsedTime;
                this.totalGameTime += targetElapsedTime;
              }
            }
            this.gameTime = new GameTime(
              this.gameTime.TotalGameTime, this.gameTime.ElapsedGameTime,
              false
            );
            try {
              this.gameTime = new GameTime(
                this.totalGameTime, targetElapsedTime,
                this.gameTime.IsRunningSlowly
              );
              this.Update(this.gameTime);
            }
            finally {
              this.lastFrameElapsedGameTime += targetElapsedTime;
              this.totalGameTime += targetElapsedTime;
            }
          }
        } else {
          TimeSpan elapsedRealTime = this.elapsedRealTime;
          try {
            this.gameTime = new GameTime(
              this.totalGameTime, this.lastFrameElapsedGameTime = elapsedRealTime,
              this.gameTime.IsRunningSlowly
            );
            this.Update(this.gameTime);
          }
          finally {
            this.totalGameTime += elapsedRealTime;
          }
        }
        if(!this.exitRequested) {
          this.DrawFrame();
        }
      }
    }

    private void UnhookDeviceEvents() {
      if(this.graphicsDeviceService != null) {
        //this.graphicsDeviceService.DeviceCreated -= new EventHandler(this.DeviceCreated);
        //this.graphicsDeviceService.DeviceResetting -= new EventHandler(this.DeviceResetting);
        //this.graphicsDeviceService.DeviceReset -= new EventHandler(this.DeviceReset);
        //this.graphicsDeviceService.DeviceDisposing -= new EventHandler(this.DeviceDisposing);
      }
    }

    protected virtual void UnloadGraphicsContent(bool unloadAllContent) {
    }

    protected virtual void Update(GameTime gameTime) {
      for(int i = 0; i < this.updateableComponents.Count; i++) {
        IUpdateable updateable = this.updateableComponents[i];
        if(updateable.Enabled) {
          updateable.Update(gameTime);
        }
      }
    }

    private void UpdateableUpdateOrderChanged(object sender, EventArgs e) {
      IUpdateable item = sender as IUpdateable;
      this.updateableComponents.Remove(item);
      int num = this.updateableComponents.BinarySearch(item, UpdateOrderComparer.Default);
      if(num < 0) {
        this.updateableComponents.Insert(~num, item);
      }
    }

    // Properties
    public GameComponentCollection Components {
      get {
        return this.gameComponents;
      }
    }

    public TimeSpan InactiveSleepTime {
      get {
        return this.inactiveSleepTime;
      }
      set {
        if(value < TimeSpan.Zero) {
          throw new ArgumentOutOfRangeException("Resources.InactiveSleepTimeCannotBeZero", "value");
        }
        this.inactiveSleepTime = value;
      }
    }

    public bool IsActive {
      get {
        return this.isActive;
      }
    }

    public bool IsFixedTimeStep {
      get {
        return this.isFixedTimeStep;
      }
      set {
        this.isFixedTimeStep = value;
      }
    }

    public bool IsMouseVisible {
      get {
        return this.isMouseVisible;
      }
      set {
        this.isMouseVisible = value;
      }
    }

    public GameServiceContainer Services {
      get {
        return this.gameServices;
      }
    }

    public TimeSpan TargetElapsedTime {
      get {
        return this.targetElapsedTime;
      }
      set {
        if(value <= TimeSpan.Zero) {
          throw new ArgumentOutOfRangeException("Resources.TargetElaspedCannotBeZero", "value");
        }
        this.targetElapsedTime = value;
      }
    }

    protected GraphicsDeviceManager graphics;

    private TimeSpan accumulatedElapsedGameTime;
    private GameClock clock;
    private bool doneFirstUpdate;
    private List<IDrawable> drawableComponents = new List<IDrawable>();
    private bool drawRunningSlowly;
    private TimeSpan elapsedRealTime;
    private bool exitRequested;
    private GameComponentCollection gameComponents;
    private GameServiceContainer gameServices = new GameServiceContainer();
    private GameTime gameTime = new GameTime();
    private IGraphicsDeviceManager graphicsDeviceManager;
    private IGraphicsDeviceService graphicsDeviceService;
    private GameHost host;
    private TimeSpan inactiveSleepTime;
    private bool inRun;
    private bool isActive;
    private bool isFixedTimeStep = true;
    private bool isMouseVisible;
    private TimeSpan lastFrameElapsedGameTime;
    private readonly TimeSpan maximumElapsedTime = TimeSpan.FromMilliseconds(500);
    private TimeSpan targetElapsedTime;
    private TimeSpan totalGameTime;
    private List<IUpdateable> updateableComponents = new List<IUpdateable>();

  }

} // namespace Nuclex

#endif